package com.gomcarter.frameworks.base.generator;

import com.google.common.base.Strings;
import freemarker.template.Configuration;
import freemarker.template.Template;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;

import java.io.*;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * 代码生成器
 * @author z.y.h
 * @deprecated 代码生成建议使用mybatis-plus-generator方案（com.yifeng.repo.micro.service.server.engine.generate.ServerEngineGenerator）
 * create on 2023/3/3 9:17
 */
@Slf4j
@Deprecated
public class CodeGenerator {
    private static final String MAIN_PREFIX = "/src/main/java/";
    private static final String MAPPER_PREFIX = "/src/main/resources/mybatis/";
    /**
     * 输出代码的绝对路径(项目所处位置), 默认为当前路径
     */
    public static String PROJECT_ABSOLUTE_DIR = null;
    /**
     * 输出代码的基础包路径
     */
    public static String BASE_PACKAGE = "com.yifeng";
    /**
     * 输出代码的子路径。最终输出的路径为：BASE_PACKAGE + SUB_PACKAGE, 例如 com.yifeng.test
     */
    public static String SUB_PACKAGE = "test";
    /**
     * 生成控制层的appId访问前缀
     */
    public static String APP_ID = "yfcloud-example";
    /**
     * 数据库地址
     */
    public static String DB_URL = "jdbc:mysql://127.0.0.1:3306/test?useSSL=true";
    /**
     * 数据库用户
     */
    public static String DB_USERNAME = "root";
    /**
     * 数据库密码
     */
    public static String DB_PASSWORD = "123456";
    /**
     * 需要生成代码的表名, 默认为全部表, 支持通配符, 多个表之间用竖线|间隔
     */
    public static String TOBE_GENERATE_TABLES = "";
    /**
     * 是否删除前缀名，比如使用通配符sys_*匹配到数据库表，生成类的时候去掉sys_
     */
    public static boolean REMOVE_PREFIX = true;
    /**
     * 支持smart-doc格式
     */
    public static boolean SUPPORT_SMART_DOC = true;
    public static boolean SUPPORT_CONTROLLER_POST = false;
    /**
     * 作者信息
     */
    public static String AUTHOR = "yifeng";
    /**
     * 外键标志, 如果Column名为， fk_user_id 则会自动生成 user 对象在目标类中
     */
    public static String FOREIGN_PREFIX = "fk_";

    private static final PackageConfig packageConfig = new PackageConfig();

    private static class PackageConfig {
        private String subPackage;
        private String entityPackage;
        private String dtoPackage;
        private String paramPackage;
        private String daoPackage;
        private String servicePackage;
        private String actionPackage;
    }
    
    /**
     * 默认生成规则: 更新的时候只包含entity和mapper路径下的文件
     */
    public static void generator() {
        generator(true, false, false, false, false, false, true);
    }

    /**
     * 全量生成规则：更新所有包路径下的文件
     */
    public static void fullCoverageGenerator() {
        generator(true, true, true, true, true, true, true);
    }

    /**
     * 自定义更新规则:
     * coverEntityPackage: 是否更新entity包
     * coverDtoPackage: 是否更新dto包
     * coverParamPackage：是否更新param包
     * coverDaoPackage：是否更新dao包
     * coverServicePackage：是否更新service包
     * coverActionPackage: 是否更新controller包
     * coverMapperDir:是否更新mapper目录
     */
    public static void generator(boolean coverEntityPackage, boolean coverDtoPackage, boolean coverParamPackage,
                                 boolean coverDaoPackage, boolean coverServicePackage, boolean coverActionPackage, boolean coverMapperDir) {
        String finalPackage = Strings.isNullOrEmpty(SUB_PACKAGE)? BASE_PACKAGE: BASE_PACKAGE + ("." + SUB_PACKAGE);
        packageConfig.entityPackage = finalPackage + ".entity";
        packageConfig.dtoPackage = finalPackage + ".dto";
        packageConfig.paramPackage = finalPackage + ".params";
        packageConfig.daoPackage = finalPackage + ".dao";
        packageConfig.servicePackage = finalPackage + ".service";
        packageConfig.actionPackage = finalPackage + ".controller";
        try (Connection conn = DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD)) {
            // get table name list
            DatabaseMetaData databaseMetaData = conn.getMetaData();
            List<Entity> entityList = new ArrayList<>();
            if ("".equals(TOBE_GENERATE_TABLES) || TOBE_GENERATE_TABLES.endsWith("*")) {
                entityList = getTableEntities(conn.getCatalog(), databaseMetaData, TOBE_GENERATE_TABLES.replace("*", ""));
            } else {
                String[] tables = TOBE_GENERATE_TABLES.split("\\|");
                for (String table : tables) {
                    entityList.add(buildTableEntity(conn.getCatalog(), table.trim(), databaseMetaData, "", FOREIGN_PREFIX));
                }
            }
            if (entityList.isEmpty()) {
                log.warn("未查询到符合条件的表");
                return;
            }
            log.info("代码开始生成,请稍加等待, 一共{}张表", entityList.size());
            for (Entity entity : entityList) {
                generate(packageConfig.entityPackage, MAIN_PREFIX, entity.className + ".java", "entity.ftl", entity, coverEntityPackage);
                generate(packageConfig.dtoPackage, MAIN_PREFIX, entity.className + "Dto.java", "smartdoc-entitydto.ftl", entity, coverDtoPackage);
                generate(packageConfig.paramPackage, MAIN_PREFIX, entity.className + "Param.java", "smartdoc-entityparam.ftl", entity, coverParamPackage);
                generate(packageConfig.paramPackage, MAIN_PREFIX, entity.className + "CreateParam.java","smartdoc-entitycreateparam.ftl", entity, coverParamPackage);
                generate(packageConfig.paramPackage, MAIN_PREFIX, entity.className + "UpdateParam.java",  "smartdoc-entityupdateparam.ftl" , entity, coverParamPackage);
                generate(packageConfig.actionPackage, MAIN_PREFIX, entity.className + "Controller.java", SUPPORT_CONTROLLER_POST ? "smartdoc-controller-post.ftl" :"smartdoc-controller.ftl", entity, coverActionPackage);
                generate(packageConfig.servicePackage, MAIN_PREFIX, entity.className + "Service.java", "service.ftl", entity, coverServicePackage);
                generate(packageConfig.daoPackage, MAIN_PREFIX, entity.className + "Mapper.java", "IMapperdao.ftl", entity, coverDaoPackage);
                generate("", MAPPER_PREFIX + SUB_PACKAGE + "/", entity.className + "Mapper.xml", "mapper.xml.ftl", entity, coverMapperDir);
            }
            log.info("代码生成完毕,代码路径={}",PROJECT_ABSOLUTE_DIR + MAIN_PREFIX + finalPackage.replace(".", "/"));
        } catch (Throwable t) {
            log.error("代码生成异常,原因=", t);
        }
    }

    private static void generate(String packageName, String prefix, String suffix, String templateName, Entity entity, boolean cover) throws Exception {
        String projectDir = Strings.isNullOrEmpty(PROJECT_ABSOLUTE_DIR) ? System.getProperty("user.dir") : PROJECT_ABSOLUTE_DIR;
        String packagePath = packageName.replace('.', '/');
        String path = projectDir + prefix + packagePath + "/" + suffix;
        File file = new File(path);
        if (!file.getParentFile().exists()) {
            file.getParentFile().mkdirs();
        }
        List<String> lines = null;
        if (file.exists()) {
            if(!cover) {
                return;
            }
            lines = readNotReplaceableLines(file);
        }
        Configuration cfg = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
        cfg.setClassForTemplateLoading(CodeGenerator.class, "/template/");
        Template hbmTemplate = cfg.getTemplate(templateName);
        Writer out = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(file.toPath()), StandardCharsets.UTF_8));
        HashMap<String, Object> dataMap = new HashMap<>(16);
        dataMap.put("entity", entity);
        hbmTemplate.process(dataMap, out);
        out.close();
        if (lines != null && lines.size() > 0) {
            replaceFile(file, lines);
        }
    }

    private static List<Entity> getTableEntities(String dbname, DatabaseMetaData dbmd, String prefix) throws Exception {
        ResultSet rs = dbmd.getTables(dbname, null, null, new String[]{"table"});
        List<String> tables = new ArrayList<String>();
        while (rs.next()) {
            String tableName = rs.getString("TABLE_NAME");
            if (Strings.isNullOrEmpty(prefix) || tableName.startsWith(prefix)) {
                tables.add(tableName);
            }
        }
        rs.close();
        List<Entity> entityList = new ArrayList<Entity>();
        for (String table : tables) {
            Entity entity = buildTableEntity(dbname, table, dbmd, prefix, FOREIGN_PREFIX);
            if (!entity.manyToMany) {
                entityList.add(entity);
            }
        }
        return entityList;
    }

    private static Entity buildTableEntity(String dbname, String table, DatabaseMetaData dbmd, String prefix, String foreignPrefix) throws Exception {
        Entity entity = new Entity();
        entity.author = AUTHOR;
        entity.createTime = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
        entity.basePackageName = BASE_PACKAGE;
        entity.entityPackageName = packageConfig.entityPackage;
        entity.dtoPackageName = packageConfig.dtoPackage;
        entity.paramPackage = packageConfig.paramPackage;
        entity.daoPackageName = packageConfig.daoPackage;
        entity.servicePackageName = packageConfig.servicePackage;
        entity.actionPackageName = packageConfig.actionPackage;
        entity.tableName = table;
        entity.className = fillUpperName(removePrefix(table, REMOVE_PREFIX ? prefix : ""));
        entity.classMapping = ("/" + APP_ID + "/" + SUB_PACKAGE + "/").replace("//", "/");
        entity.classInstanceName = fillLowerName(removePrefix(table, REMOVE_PREFIX ? prefix : ""));

        ResultSet rs = dbmd.getPrimaryKeys(dbname, null, table);
        while (rs.next()) {
            if (entity.idColumn != null) {
                entity.manyToMany = true;
                return entity;
            }
            entity.idColumn = rs.getString("COLUMN_NAME");
        }
        rs.close();
        rs = dbmd.getImportedKeys(dbname, null, table);
        while (rs.next()) {
            Entity.AliasProperty aprop = new Entity.AliasProperty();
            aprop.primaryTableName = rs.getString("PKTABLE_NAME");
            aprop.primaryClassName = fillUpperName(removePrefix(aprop.primaryTableName, foreignPrefix));
            aprop.columnName = rs.getString("FKCOLUMN_NAME");
            String str = fillLowerName(aprop.columnName);
            aprop.propName = str.endsWith("Id") ? str.substring(0, str.length() - 2) : str;
            aprop.getMethod = "get" + fillFirstUpper(aprop.propName);
            aprop.setMethod = "set" + fillFirstUpper(aprop.propName);
            entity.aPropList.add(aprop);
        }
        rs.close();
        rs = dbmd.getExportedKeys(dbname, null, table);
        while (rs.next()) {
            if (table.equals(rs.getString("PKTABLE_NAME"))) {
                Entity.SetProperty sprop = new Entity.SetProperty();
                sprop.foreignTableName = rs.getString("FKTABLE_NAME");
                sprop.foreignClassName = fillUpperName(removePrefix(sprop.foreignTableName, FOREIGN_PREFIX));
                sprop.foreignColumnName = rs.getString("FKCOLUMN_NAME");
                sprop.propName = fillFirstLower(sprop.foreignClassName) + "Set";
                sprop.getMethod = buildGetMethod(sprop.propName);
                sprop.setMethod = buildSetMethod(sprop.propName);
                buildManyToMany(dbname, sprop, dbmd);
                entity.sPropList.add(sprop);
                entity.hasSetType = true;
            }
        }
        rs.close();
        rs = dbmd.getColumns(dbname, null, table, "%");
        while (rs.next()) {
            String columnName = rs.getString("COLUMN_NAME");
            if ("version".equalsIgnoreCase(columnName)) {
                entity.hasHibernateVersion = true;
                entity.versionLock = "#{version}";
                continue;
            }
            entity.columnName = columnName;
            // 对于视图没有主键的问题，默认以第一列为主键 Modified by Squid Hex
            // Now you can generate code by view
            String type = rs.getString("TYPE_NAME");
            Class<?> kls = defaultTypeMap.get(type);
            if (kls == null) {
                System.out.println("The column {" + columnName + "} has unknown Database DataType: " + type +
                        ", String.class are replaced default,  you can call CodeGenerator.addTypeMapping(.., ..) to correct it also");
                kls = String.class;
            }
            if (entity.idColumn == null) {
                entity.idColumn = columnName;
                entity.idName = fillLowerName(columnName);

                entity.idType = kls.getName();
                entity.idSimpleType = kls.getSimpleName();
                entity.idGetMethod = buildGetMethod(entity.idName);
                entity.idSetMethod = buildSetMethod(entity.idName);
                entity.idAutoIncrement = !"NO".equals(rs.getString("IS_AUTOINCREMENT"));
            } else {
                if (columnName.equals(entity.idColumn)) {
                    entity.idName = fillLowerName(entity.idColumn);
                    entity.idType = kls.getName();
                    entity.idSimpleType = kls.getSimpleName();
                    entity.idGetMethod = buildGetMethod(entity.idName);
                    entity.idSetMethod = buildSetMethod(entity.idName);
                    entity.idAutoIncrement = !"NO".equals(rs.getString("IS_AUTOINCREMENT"));
                } else {
                    Entity.Property prop = new Entity.Property();
                    prop.column = columnName;
                    prop.propName = fillLowerName(prop.column);
                    prop.propType = kls.getName();
                    prop.simpleType = kls.getSimpleName();
                    if ("BigDecimal".equals(prop.simpleType)) {
                        entity.hasDecimalType = true;
                    } else if ("Date".equals(prop.simpleType)) {
                        entity.hasDateType = true;
                    }
                    prop.notAllowNull = "NO".equals(rs.getString("IS_NULLABLE")) ?
                            (rs.getString("COLUMN_DEF") == null ? "true" : "false")
                            : "false";
                    prop.note = rs.getString("REMARKS");
                    prop.getMethod = buildGetMethod(prop.propName, "Boolean".equals(prop.simpleType));
                    prop.setMethod = buildSetMethod(prop.propName);
                    for (Entity.AliasProperty aprop : entity.aPropList) {
                        if (prop.column.equals(aprop.columnName)) {
                            prop.manyToOne = true;
                            break;
                        }
                    }
                    entity.propList.add(prop);
                }
            }
        }
        rs.close();
        return entity;
    }

    private static List<String> readNotReplaceableLines(File file) throws IOException {
        FileReader fr = new FileReader(file);
        BufferedReader br = new BufferedReader(fr);
        List<String> lines = null;
        boolean start = false;
        String line;
        while ((line = br.readLine()) != null) {
            if (line.contains("//@NotReplaceableStart")) {
                lines = new ArrayList<String>();
                start = true;
            }
            if (start) {
                lines.add(line);
            }
            if (line.contains("//@NotReplaceableEnd")) {
                start = false;
            }
        }
        br.close();
        fr.close();
        return lines;
    }

    private static void replaceFile(File file, List<String> lines) throws IOException {
        File destFile = new File(file.getPath() + ".temp");
        if (!file.exists()) {
            return;
        }
        FileReader fileReader = new FileReader(file);
        BufferedReader bufferedReader = new BufferedReader(fileReader);

        FileWriter fw = new FileWriter(destFile);
        String tempLines;

        boolean replace = false;
        while ((tempLines = bufferedReader.readLine()) != null) {
            if (tempLines.contains("//@NotReplaceableStart")) {
                replace = true;
                for (String change : lines) {
                    fw.write(change + "\r\n");
                }
            }
            if (!replace) {
                fw.write(tempLines + "\r\n");
            }
            if (tempLines.contains("//@NotReplaceableEnd")) {
                replace = false;
            }
        }
        fw.close();
        bufferedReader.close();
        fileReader.close();
        FileUtils.copyFile(destFile, file);
        destFile.delete();
    }

    private static void buildManyToMany(String dbname, Entity.SetProperty sprop, DatabaseMetaData dbmd) throws Exception {
        ResultSet rs = dbmd.getPrimaryKeys(dbname, null, sprop.foreignTableName);
        int primaryKeyNumber = 0;
        while (rs.next()) {
            primaryKeyNumber++;
        }
        if (primaryKeyNumber > 1) {
            sprop.manyToMany = true;
            rs.close();

            rs = dbmd.getImportedKeys(dbname, null, sprop.foreignTableName);
            while (rs.next()) {
                if (!sprop.foreignColumnName.equals(rs.getString("FKCOLUMN_NAME"))) {
                    sprop.manyToManyTable = sprop.foreignTableName;
                    sprop.foreignTableName = rs.getString("PKTABLE_NAME");
                    sprop.foreignClassName = fillUpperName(removePrefix(sprop.foreignTableName, ""));
                    sprop.propName = fillFirstLower(sprop.foreignClassName) + "Set";
                    sprop.getMethod = buildGetMethod(sprop.propName);
                    sprop.setMethod = buildSetMethod(sprop.propName);
                    sprop.manyForeignColumnName = rs.getString("FKCOLUMN_NAME");
                    break;
                }
            }
            rs.close();
        }
    }

    private static String fillUpperName(String underline) {
        String lower = underline2camel(underline);
        return fillFirstUpper(lower);
    }

    private static String fillLowerName(String underline) {
        return underline2camel(underline);
    }

    private static String fillFirstUpper(String name) {
        return name.substring(0, 1).toUpperCase() + name.substring(1);
    }

    private static String fillFirstLower(String name) {
        return name.substring(0, 1).toLowerCase() + name.substring(1);
    }

    private static String underline2camel(String underline) {
        StringBuilder sb = new StringBuilder();
        String temp = underline;
        while (true) {
            int index = temp.indexOf("_");
            if (index == -1) {
                sb.append(temp);
                break;
            } else {
                sb.append(temp.substring(0, index));
                sb.append(temp.substring(index + 1, index + 2).toUpperCase());
                temp = temp.substring(index + 2);
            }
        }
        return sb.toString();
    }

    private static String removePrefix(String tableName, String prefix) {
        if (tableName.startsWith(prefix)) {
            return tableName.substring(prefix.length());
        } else {
            return tableName;
        }
    }

    private static String removeSuffix(String tableName, String suffix) {
        if (tableName.endsWith(suffix)) {
            return tableName.substring(0, tableName.length() - suffix.length());
        } else {
            return tableName;
        }
    }

    private static String buildGetMethod(String field, boolean isBoolean) {
        return "get" + field.substring(0, 1).toUpperCase() + field.substring(1);
    }

    private static String buildGetMethod(String field) {
        return buildGetMethod(field, false);
    }

    private static String buildSetMethod(String field) {
        return "set" + field.substring(0, 1).toUpperCase() + field.substring(1);
    }

    private static final Map<String, Class> defaultTypeMap = new HashMap<String, Class>() {{
        put("LONGTEXT", String.class);
        put("INT", Integer.class);
        put("BIGINT", Long.class);
        put("DATE", Date.class);
        put("DATETIME", Date.class);
        put("TEXT", String.class);
        put("VARCHAR", String.class);
        put("DECIMAL", BigDecimal.class);
        put("BIT", Boolean.class);
        put("TIMESTAMP", Date.class);
        put("FLOAT", Float.class);
        put("DOUBLE", Double.class);
        put("MEDIUMTEXT", String.class);
        put("CHAR", String.class);
        put("ENUM", String.class);
        put("BLOB", String.class);
        put("TINYINT", Short.class);
        put("INT UNSIGNED", Integer.class);
        put("BIGINT UNSIGNED", Long.class);
        put("TINYINT UNSIGNED", Short.class);
    }};

    public static void addTypeMapping(String dbType, Class javaType) {
        defaultTypeMap.put(dbType, javaType);
    }
}
